require 'java'
require 'rake'
require 'jrake'
require 'set'

# Builds rake targets submitted as HTTP requests
  
class BuildHandler < org.mortbay.jetty.Handler

  # todo: Should extend AbstractHandler
  #   (need JRuby to support extending abstract classes)
  
  def initialize
    super()
    
    @task_coordinator = TaskCoordinator.new(1)
    
    @rakefile = 'rakefile.rb'
  end

  def handle(target,request,response,dispatch)
    begin
      Rake.application.clear
      load @rakefile
    
      servlet_info = JRake.lookup_servlet(request.path_info)
      if servlet_info 
        handle_servlet(request,response,servlet_info)
      else
        handle_build(request,response)
      end
    rescue Exception => e
      response.content_type = 'text/plain'
      response.status = javax.servlet.http.HttpServletResponse::SC_INTERNAL_SERVER_ERROR
      if defined?(e.cause) && e.cause
        e = e.cause while e.cause
        e.print_stack_trace(response.writer)
      else
        response.writer.write("#{e.class.name}: #{e.message}\n")
        response.writer.write(e.backtrace.join("\n"))
      end
    end
    request.handled = true    
  end

  def handle_build(request,response)    
    # Route all log messages to the client
    JRake::with_logger(response.writer) do
      begin
        path_info = request.path_info[1..-1] # remove initial slash
        target_names = java.net.URLDecoder.decode(path_info).split 
        targets = target_names.map { |target_name| lookup_target(target_name) }
        targets.each { |target| @task_coordinator.execute(target) }        
      rescue JRake::FailureException => e
        JRake::log "BUILD FAILED"  
      rescue Exception => e
        JRake::log "#{e.class.name}:#{e.message}\n    "
        JRake::log e.backtrace.join("\n    ")
        JRake::log "BUILD FAILED"  
      end
    end
    
  end
  
  def handle_servlet(request,response,info)
    # Shift path element from path_info to servlet_path
    request.servlet_path += info.servlet_path
    if info.servlet_path.length == request.path_info.length
      request.path_info = nil
    else
      request.path_info = request.path_info[info.servlet_path.length, request.path_info.length-info.servlet_path.length]
    end
    
    log = java.io.StringWriter.new
    begin
      if request.get_header("cache-control") =~ /max-age=0|no-cache/
        JRake::with_logger(log) do
          lookup_target(info.build_target).invoke
        end
      end
      servlet_class = JRake::load_class(info.class_path, info.servlet_class_name)      
      servlet = servlet_class.new_instance      
      param_types = JRake::to_java_array(java.lang.Class, [
          javax.servlet.ServletRequest,
          javax.servlet.ServletResponse
      ])
      method = servlet_class.get_method("service", param_types)
      args = JRake::to_java_array(java.lang.Object, [request,response])
    	method.invoke(servlet, args)              
    rescue JRake::FailureException => e
      response.content_type = 'text/plain'
      response.status = javax.servlet.http.HttpServletResponse::SC_INTERNAL_SERVER_ERROR
      response.writer.write(log.to_s)
    end
  end
    
  def lookup_target(target_name)
    target = Rake.application.lookup(target_name)
    if target.nil?
      JRake::log "Target not found: #{target_name}\n"
      JRake::fail
    end
    return target
  end
  
  def start
  end
  
  def stop
    @task_coordinator.shutdown
  end
  
  def setServer(server)
    @server = server
  end
  
  def getServer
    @server
  end
end

module Rake
  class Task
    def prerequisite_tasks
      prerequisites.map { |task_name| Rake.application.lookup(task_name) }
    end
  end
end

TaskResult = Struct.new(:task, :exception)

class TaskRunner < java.lang.Runnable
  attr_reader :task
  
  def initialize(task, queue)
    super()
    
    @task = task
    @queue = queue
    @exception = nil
  end
  
  def run
    begin
      @task.execute
    rescue Exception => exception
      puts exception
      @exception = exception
    ensure
      @queue.add(TaskResult.new(@task, @exception))
    end
  end
end

class TaskCoordinator
  
  def initialize(pool_size=2)
    @thread_pool = java.util.concurrent.Executors.newFixedThreadPool(pool_size)
  end
  
  def execute(root)
    @result_queue = java.util.concurrent.LinkedBlockingQueue.new
    @leafs = Set.new
    @children = {}
    @parents = {}
    
    analyze_dependencies(root) 
    
    @leafs.each do |leaf|
      execute_task(leaf)
    end
    
    while (result = @result_queue.take).task != root
      @parents[result.task].each do |posttask|
        list = @children[posttask]
        list.delete(result.task)
        if list.empty? 
          @children.delete(posttask)
          execute_task(posttask)
        end
      end
      
      @parents.delete(result.task)
    end
  end
  
  def shutdown
    @thread_pool.shutdown
  end

private
  
  # Stores the relationship between the tasks involved in this 
  # build, using the "children" and "parents" hash tables
  def analyze_dependencies(task, analyzed=Set.new)
    return if analyzed.member?(task)
    
    analyzed << task
    
    prereqs = task.prerequisite_tasks
    if prereqs.empty?
      @leafs << task
    else
      prereqs.each do |child| 
        (@children[task] ||= Set.new) << child
        (@parents[child] ||= Set.new) << task
        analyze_dependencies(child,analyzed)
      end
    end
  end
  
  # Wraps the task in a TaskRunner and passes it to the
  # thread pool for execution
  def execute_task(task)
    @thread_pool.execute(TaskRunner.new(task, @result_queue))
  end
end


if __FILE__ == $0 then
  server = org.mortbay.jetty.Server.new(3030)
  handler = BuildHandler.new
  server.set_handler(handler)

  server.start
  puts "\nHit <enter> stop server\n\n"
  $stdin.readline
  server.stop
end
